﻿package mlc.haxe.output.preprocessing;
import haxe.PosInfos;
import mlc.haxe.output.ConsoleOutputHandler;

/**
 * A basic, not so intelligent, trace preprocessor.
 */
class BasicTracePreprocessor implements ConsoleTracePreprocessor {
	/**
	 * The highest depth used when peeking inside collections.
	 */
	private static inline var MAXIMAL_COLLECTION_DEPTH:Int = 2;
	/**
	 * The highest number of elements in a collection to display.
	 */
	private static inline var MAXIMAL_COLLECTION_ELEMENTS:Int = 6;
	/**
	 * Creates a new basic trace preprocessor.
	 */
	public function new():Void {
	}
	public function callOutputHandler(outputHandler:ConsoleOutputHandler, value:Dynamic, positionInformation:PosInfos):Void {
		// If the passed value is null, call the writeNull method.
		if (value == null) {
			outputHandler.writeNull(positionInformation);
		// If the passed value is a string, apply some logic to determine whether the writeString or writeXml method should be
		// called.
		} else if (Std.is(value, String)) {
			// If the passed value might contain an XML document, try to parse it as XML.
			if (determineMightBeXML(value)) {
				var xml:Xml = null;
				try {
					xml = Xml.parse(value);
				} catch (error:Dynamic) {
				}
				// If the parsing succeeded, call the writeXml method and return.
				if (xml != null) {
					outputHandler.writeXml(xml, positionInformation);
					return;
				}
			}
			// If the passed value is not an XML document, call the writeString method.
			outputHandler.writeString(value, positionInformation);
		// If the passed value is an integer, call the writeInt method.
		} else if (Std.is(value, Int)) {
			outputHandler.writeInt(value, positionInformation);
		// If the passed value is a float, call the writeFloat method.
		} else if (Std.is(value, Float)) {
			outputHandler.writeFloat(value, positionInformation);
		// If the passed value is a bool, call the writeBool method.
		} else if (Std.is(value, Bool)) {
			outputHandler.writeBool(value, positionInformation);
		// If the passed value is Xml, call the writeXml method.
		} else if (Std.is(value, Xml)) {
			outputHandler.writeXml(value, positionInformation);
		// If the passed value is a AVM2 XML object, convert it to a haXe Xml object and then call the writeXml method.
		#if flash9
		} else if (Std.is(value, flash.xml.XML)) {
			outputHandler.writeXml(Xml.parse(value.toXMLString()), positionInformation);
		#end
		// If the passed value is a function, call the writeString method.
		} else if (Reflect.isFunction(value)) {
			outputHandler.writeString("[function]", positionInformation);
		// If the passed value is an AVM2 error, write the error message in red.
		#if flash9
		} else if (Std.is(value, flash.errors.Error)) {
			writeError(value.message, outputHandler, positionInformation);
		#end
		// If the passed value is none of the above, convert it to a string and then call the writeString method.
		} else {
			outputHandler.writeString(generateStringRepresentation(value), positionInformation);
		}
	}
	/**
	 * Determines whether the passed value might be a string containing an XML document, or not. Returns true if the passed value
	 * might be XML; false otherwise. Note that if this method returns true, it is not ensured that the passed value is actually
	 * valid XML. However, if this method returns false, it certainly is not.
	 */
	private inline function determineMightBeXML(value:String):Bool {
		return StringTools.ltrim(value).charCodeAt(0) == 60;
	}
	/**
	 * Generates a string representation for values that can not be converted to one of the types supported by the console.
	 */
	private function generateStringRepresentation(value:Dynamic, ?depth:Int):String {
		// If the passed value is either an array or a list, join the elements inside the collection.
		if (Std.is(value, Array) || Std.is(value, List)) {
			// If no depth is specified, use 0.
			if (depth == null) {
				depth = 0;
			// If the depth is the maximal depth for collections, return a suspension point instead of generating a full string
			// representation. This prevents infinite string representations, in some cases.
			} else if (depth == MAXIMAL_COLLECTION_DEPTH) {
				return "[...]";
			}
			var stringBuffer:StringBuf = new StringBuf();
			// Create a variable that stores the number of elements that have been added to the string representation.
			var numberOfElements:Int = 0;
			// Add an opening square bracket to the string representation.
			stringBuffer.add("[");
			var iterator:Iterator<Dynamic> = value.iterator();
			for (element in iterator) {
				// Add a string representation of the element to the string representation.
				stringBuffer.add(generateStringRepresentation(element, depth + 1));
				++numberOfElements;
				// If this collection also contains at least one element after this one, add a comma to the string representation.
				if (iterator.hasNext()) {
					stringBuffer.add(", ");
					// If the maximal number of elements for collections has been reached, add a suspension point and stop iterating.
					if (numberOfElements == MAXIMAL_COLLECTION_ELEMENTS) {
						stringBuffer.add("...");
						break;
					}
				}
			}
			// Add a closing square bracket to the string representation.
			stringBuffer.add("]");
			return stringBuffer.toString();
		// If the passed value is not an array or a list, use the Std.string method to generate a string representation.
		} else {
			return Std.string(value);
		}
	}
	#if debug
	private function toString():String {
		return "[ConsoleOutputPreprocessor]";
	}
	#end
	private function writeError(message:String, outputHandler:ConsoleOutputHandler, positionInformation:PosInfos):Void {
		// Write the actual error message in red.
		outputHandler.writeAdvancedString(message, 0xF95A61, false);
		// Split the package and the actual class name.
		var regularExpression:EReg = ~/(.+)\.([^\.]+)$/;
		// If the regular expression above matches, there is an actual package.
		if (regularExpression.match(positionInformation.className)) {
			outputHandler.writeAdvancedString(" at " + regularExpression.matched(1) + "::" + regularExpression.matched(2) + "/" + positionInformation.methodName + "():" + positionInformation.lineNumber, 0xFFFFFF, true);
		// If the regular expression above doesn't match, there is no package.
		} else {
			outputHandler.writeAdvancedString(" at " + positionInformation.className + "/" + positionInformation.methodName + "():" + positionInformation.lineNumber, 0xFFFFFF, true);
		}
	}
}